#include "Merger.h"
#include "../utils/FileSystem.h"
#include <math.h>
#include <sstream>

#include <iostream>
#include <cerrno>
#include <cstring>


Merger::Merger() {
	this->_cantArchivosExistentes = -1;
	this->_carpetaParticiones = "";
	this->_ordenParticionInit = 1;
	this->_nomArchFinal = "";
}


Merger::~Merger() {
	this->_cantArchivosExistentes = 0;
	this->_carpetaParticiones = "";
	this->_ordenParticionInit = 1;
	this->_nomArchFinal = "";
	descargarArchivos();
}


void Merger::init(std::string carpetaParticiones, std::string nomArchFinal){
	this->_cantArchivosExistentes = 0;
	this->_carpetaParticiones = carpetaParticiones;
	this->_nomArchFinal = nomArchFinal;
}


// Realiza la fusion de las particiones
void Merger::fusionarParticiones(int cantParticiones, int fusionesSimultaneas){
	if (fusionesSimultaneas < 2)
		throw std::runtime_error("No se pueden fusionar menos de 2 archivos");

	// Evita un problema si se trata de ingresar una cantidad muy alta
	this->_cantArchivosExistentes = cantParticiones;
	if (fusionesSimultaneas > this->_cantArchivosExistentes){
		fusionesSimultaneas = this->_cantArchivosExistentes;
	}

	// Redondea hacia arriba la cantidad de etapas
	int cantidadEtapas = calcularCantEtapas(fusionesSimultaneas, this->_cantArchivosExistentes);

	// Ejecuta el proceso de cada etapa
	for (int etapa = 1; etapa <= cantidadEtapas; ++etapa){

		// Cantidad de archivos para procesar en la etapa actual
		int cantDeArchivosParciales = 0;
		FileSystem::crearCarpetaFusion(etapa);

		// Mientras haya particiones para procesar en la etapa
		while (this->_ordenParticionInit <= this->_cantArchivosExistentes){

			// Para las fusiones simultaneas
			cargarArchivos(fusionesSimultaneas, etapa);

			// Fusiona todos los archivos
			cantDeArchivosParciales++;
			fusionarArchivos(cantDeArchivosParciales, etapa);

			descargarArchivos();
		}

		// Inicializa la siguiente etapa
		this->_ordenParticionInit = 1;
		this->_cantArchivosExistentes = cantDeArchivosParciales;
	}

	// Copia el archivo resultado a la raiz
	moverArchResultado(cantidadEtapas);
}


// Realiza la fusion de todos los archivos cargados en el vector de particiones
void Merger::fusionarArchivos(int nroFusion, int etapa){

	// Inicializa el vector de ocurrencias
	std::vector<RegistroOcurrencia> vecOcurrencias;
	leerTerminos(vecOcurrencias);

	// Abre el archivo de salida
	std::string nomArchSalida = FileSystem::getNombreFusion(nroFusion, etapa);
	this->fout.open(nomArchSalida.c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary);

	RegistroOcurrencia ocurrAnterior;
	while ( existenTerminos(vecOcurrencias) ){

		// Actualiza el vector de ocurrencias leyendo las particiones
		leerTerminos(vecOcurrencias, ocurrAnterior);

		// Busca el menor de los terminos y lo guarda en el archivo de salida
		RegistroOcurrencia ocurrenciaMinima = getTerminoMinimo(vecOcurrencias);
		this->fout << ocurrenciaMinima;

		// Lleva cuenta del termino que se guardó para saber cual partición leer
		ocurrAnterior = ocurrenciaMinima;
	}

	// Cierra el archivo de salida
	this->fout.close();
}


// Carga la cantidad de particiones en el vector para realizar la fusion
void Merger::cargarArchivos(int cantArch, int etapa){

	// Para que carge la cantidad correcta de archivos con el numero de orden correcto
	int nroArchFinal = cantArch + this->_ordenParticionInit -1;
	if (nroArchFinal > this->_cantArchivosExistentes){
		nroArchFinal = this->_cantArchivosExistentes;
	}

	// Cuidado que no trate de cargar mas archivos de los que existen
	for (int i = this->_ordenParticionInit; i <= nroArchFinal; i++){

		// Abre el archivo a procesar
		std::string nomArch = getNomArchivoProcesar(i, etapa);
		std::fstream* fin = new std::fstream(nomArch.c_str(), std::ifstream::in | std::ifstream::binary);
		if (!fin->good()) throw std::runtime_error("Error al abrir el archivo " +nomArch);

		this->_vecArchEntrada.push_back(fin);
	}

	// Actualiza cual es el nro de la proxima particion inicial
	this->_ordenParticionInit += cantArch;
}


void Merger::descargarArchivos(){
	std::vector<std::fstream*>::iterator it;
	for (it = this->_vecArchEntrada.begin(); it != this->_vecArchEntrada.end(); ++it){

		std::fstream& fin = **it;
		if (fin.is_open()) fin.close();

		delete *it;
		*it = NULL;
	}

	// Lo deja cero km
	this->_vecArchEntrada.clear();
}


bool Merger::existenTerminos(std::vector<RegistroOcurrencia> &terminos) const{
	std::vector<RegistroOcurrencia>::const_iterator cit;
	for (cit = terminos.begin(); cit != terminos.end(); ++cit){

		// Existe al menos un termino menor que el maximo imposible
		if ( (*cit).getTermino() < RegistroOcurrencia::TERMINO_MAXIMO_IMPOSIBLE() ) return true;
	}

	return false;
}


// Actualiza el vector de terminos leyendo del archivo que corresponde según cual haya sido el anterior termino leido
void Merger::leerTerminos(std::vector<RegistroOcurrencia> &vecOcurrencias, RegistroOcurrencia ocurrAnterior){

	// Esto pasa porque recien empezó o porque ya terminó, en los dos casos no debe hacer nada
	if (ocurrAnterior.getTermino() == "") return;
	if (ocurrAnterior.getTermino() == RegistroOcurrencia::TERMINO_MAXIMO_IMPOSIBLE()) return;

	// Busco en el vector de terminos la posicion del termino que fue leido con anterioridad
	unsigned int pos;
	for (pos = 0; pos < vecOcurrencias.size(); ++pos){
		RegistroOcurrencia ro = vecOcurrencias[pos];

		// Busca la posicion a la que corresponde la ocurrencia que fue leida anteriormente
		if (ro.getTermino() == ocurrAnterior.getTermino() && ro.getDocID() == ocurrAnterior.getDocID()) break;
	}

	// Leo el archivo que corresponde a ese termino
	RegistroOcurrencia ro;
	std::fstream* pFin = this->_vecArchEntrada[pos];
	if (pFin->is_open()){
		(*pFin) >> ro;

		if (pFin->eof()) pFin->close();
	}

	// Si ya se leyó todo el archivo coloco el registro maximo imposible para evitar mas problemas
	if (!pFin->is_open()) ro.setTermino(RegistroOcurrencia::TERMINO_MAXIMO_IMPOSIBLE());

	// No se leyó ningun termino desde el archivo...
	if (ro.getTermino() == "") throw std::runtime_error("Falló la lectura desde el archivo");

	// Guardo el termino leido en el vector
	vecOcurrencias[pos] = ro;
}


// Inicializa el vector de ocurrencias
void Merger::leerTerminos(std::vector<RegistroOcurrencia> &vecOcurrencias){
	// Recupera uno de cada archivo
	std::vector<std::fstream*>::iterator it;
	for (it = this->_vecArchEntrada.begin(); it != this->_vecArchEntrada.end(); ++it){

		RegistroOcurrencia ro;
		(**it) >> ro;

		vecOcurrencias.push_back(ro);
	}
}


// Recorre todo el vector hasta encontrar la menor de las palabras
RegistroOcurrencia Merger::getTerminoMinimo(std::vector<RegistroOcurrencia> &vecOcurrencias) const {

	std::vector<RegistroOcurrencia>::const_iterator cit;
	std::vector<RegistroOcurrencia>::const_iterator citMin = vecOcurrencias.begin();

	for (cit = vecOcurrencias.begin(); cit != vecOcurrencias.end(); ++cit){
		// Encontré un nuevo valor minimo
		if ( cit->getTermino() < citMin->getTermino() ) citMin = cit;
	}

	// Devuelve una copia del minimo termino encontrado
	return *citMin;
}


// Copia el resultado a la raiz del programa, puede o no incluir el orden de los terminos
void Merger::moverArchResultado(int etapa) const{
	std::string archOrigen = FileSystem::getNombreFusion(1, etapa);

	std::ifstream fin(archOrigen.c_str(), std::ifstream::in | std::ofstream::binary);
	std::ofstream fout(this->_nomArchFinal.c_str(), std::ofstream::out | std::ofstream::binary | std::ofstream::trunc);

	fout << fin.rdbuf();

	fin.close();
	fout.close();
}


int Merger::calcularCantEtapas(int nivelMerge, int cantTotalArchivos) const{
	// Para no tener problemas con las fracciones
	float nivel = nivelMerge;

	int cantEtapas = 0;
	int resul = cantTotalArchivos;

	while (resul != 1){
		resul = ceil( resul / nivel );
		cantEtapas++;
	}

	return cantEtapas;
}


// Devuelve el nombre del archivo que se debe procesar en la etapa señalada
std::string Merger::getNomArchivoProcesar(int orden, int etapa) const{

	if (etapa == 1) {
		return this->_carpetaParticiones + "/" + FileSystem::getNombreParticion(orden);
	}

	// Porque "-1"? porque estoy en la etapa "x" pero necesito los archivos de la etapa "x-1"
	return FileSystem::getNombreFusion(orden, etapa-1);
}


